refactor(git_utils): shared git-error panel printer + cwd= over chdir guards - #596
Conversation
Add `_print_git_error` and use it from both `git_checkout_tag` and `checkout_pr` except blocks, deduplicating the two structurally-identical rich error panels. Replace the `os.getcwd`/`os.chdir`/`finally` scaffolding in both functions with `cwd=repo_path` on every `subprocess.run` call. This removes process-global cwd mutation (safer for any future threaded/concurrent use) and drops the `import os` that only existed for the chdir dance.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 19 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 1 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
Panel: 5/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), claude-opus-4-8-thinking-xhigh:edge-case (parse_error), kimi-k2.5:edge-case (empty)
…nt injection The fork-controlled PR head branch name (pr_info.head_branch, from the GitHub API and controllable by the PR author) was passed positionally to 'git fetch' on both the fork and non-fork paths. A name beginning with '-' would be parsed as a git option; 'comfy install --pr' runs against untrusted forks. Add a '--' end-of-options separator before the refspec and lock it in with regression assertions. Addresses cursor-review panel Medium finding (BE-4357). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bigcat88
left a comment
There was a problem hiding this comment.
Requesting changes on one line — everything else here is verified and good, including a security fix that's better than the description lets on.
Blocking: this PR turns main's ruff_check red
ruff_check is failing, and it's PR-caused, not stale CI. The branch rewrites an already-correctly-formatted docstring into a form ruff rejects:
- """A rate-limited 403 surfaces as GitHubRateLimitError, not a silent "no PR found\""""
+ """A rate-limited 403 surfaces as GitHubRateLimitError, not a silent "no PR found\" """Proof it's this branch and not the environment — same ruff, both trees, this branch merged with current main:
$ uvx ruff@0.15.15 format --diff . # on origin/main
270 files already formatted
$ uvx ruff@0.15.15 format --diff . # on this branch
1 file would be reformatted
Why it passed for you: CI pins ruff 0.15.15 (.github/workflows/ruff_check.yml) and runs ruff format --diff, but 0.15.13 accepts this line — I hit the same false-green locally until I matched the pin. Your "ruff check clean" claim is literally true; it's ruff format that fails. Worth running uvx ruff@0.15.15 format --diff . before pushing.
Revert that one line to main's version and this is ready — nothing else needs to change.
The -- fix is a real RCE guard, and it isn't in the description at all
Commit 667887d (-- separator on git fetch) is the most valuable thing in this PR and the body never mentions it. I verified the vulnerability is real against live git 2.39.5, using a branch name of --upload-pack=touch <path>:
main's form: git fetch origin '--upload-pack=touch …'
→ command EXECUTED (marker file created)
this branch: git fetch origin -- '--upload-pack=touch …'
→ fatal: invalid refspec '--upload-pack=touch …' (blocked)
pr_info.head_branch comes straight from the GitHub API and flows into comfy install --pr against forks, and git check-ref-format accepts refs/heads/--upload-pack=x, so the CLI can't lean on git or GitHub refusing the name. This is worth its own line in the description (and arguably its own PR) so it isn't buried in a refactor.
I checked the calls you did not add -- to, and they're all safe by construction: the two git checkout -B start-points are prefixed (origin/…, remote/…), local_branch is prefixed pr-, and git remote add <name> <url> rejects a dash-leading URL with error: unknown option before doing anything (verified). So the two fetch calls were the whole hole.
Also confirmed git fetch <remote> -- <branch> is accepted by git — the tests mock subprocess.run, so nothing in the suite would have caught it if the syntax were wrong.
The cwd= refactor checks out
Verified against real git repos, not mocks:
- real repo + tag → returns
True, process cwd unchanged - missing tag → returns
Falsevia the shared panel - both panels render;
checkout_pr's is byte-identical as claimed
Fixing the Text.append(f"[cyan]{tag}[/cyan]") bug is a genuine improvement — Text.append doesn't parse markup, so users really were seeing literal [cyan]v1.2.3[/cyan]. And because Text.append doesn't parse markup, a tag or PR title containing [...] can't raise MarkupError through the new helper.
One description nit: the body says cwd= "removes the failure mode where os.chdir(repo_path) itself throws (e.g. missing dir)". It doesn't — the throw just moves from os.chdir to subprocess.run(cwd=…). Both main and this branch raise an uncaught FileNotFoundError for a missing repo dir; I checked both. No regression, but the claim overstates it — what you actually removed is the process-global cwd mutation, which is a good enough reason on its own.
(Unrelated and pre-existing on both branches: import comfy_cli.git_utils on its own raises ImportError from a cycle with comfy_cli/command/github/__init__.py. Not yours, not in scope — just noting it since it bites anyone importing the module directly.)
…15.15 The branch had accidentally reformatted the `test_find_pr_by_branch_rate_limit` docstring, adding a space before the closing triple-quote. ruff format 0.15.15 (the version CI pins) rejects that form, turning `ruff_check` red; 0.15.13 accepts both, which is why it looked clean locally. Restore main's version -- unrelated to this PR's purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up: I found the root cause, and it isn't a stray editor touch — your local ruff is rewriting that line for you. I ran each ruff version's So ruff 0.14.x turns """… not a silent "no PR found\""""into """… not a silent "no PR found\" """and 0.15.15 rejects that. Running Fix that outlasts this PR: pin your local ruff to CI's version. Either uvx ruff@0.15.15 check . && uvx ruff@0.15.15 format --diff .or add For this PR specifically: revert that one line to |
bigcat88
left a comment
There was a problem hiding this comment.
Fixed — clearing my earlier changes-requested.
The docstring is now byte-identical to main:
this branch: 183: """A rate-limited 403 … not a silent "no PR found\""""
main: 183: """A rate-limited 403 … not a silent "no PR found\""""
and CI's pinned toolchain is clean on both halves:
$ uvx ruff@0.15.15 check . -> All checks passed!
$ uvx ruff@0.15.15 format --diff . -> 272 files already formatted
The fix commit touched exactly 1 file, 1 line — nothing else moved. Both git fetch … -- guards are still in place (git_utils.py:128 and :150), so the argument-injection fix I verified earlier is intact: a branch named --upload-pack=touch <path> executed on main's form and is rejected as an invalid refspec here.
Full suite re-run green on this branch merged with current main.
Everything from the earlier review stands — the cwd= refactor checked out against real git repos (process cwd unchanged, False on a bad tag, both panels rendering), and the Text.append markup bug fix is a genuine improvement.
Worth acting on the root cause I posted separately so this doesn't recur: your local ruff is 0.14.x, which rewrites that docstring automatically; 0.15.x leaves it alone. Pinning to ruff==0.15.15 locally will stop it re-appearing in any PR that touches a file with a docstring ending in an escaped quote.
ELI-5
Two functions in
git_utils.py—git_checkout_tagandcheckout_pr— each did the same two clunky things: (1) built a nearly-identical fancy red error box by hand, and (2)cd-ed the whole process into the repo folder to run git, thencd-ed back in afinally. This PR pulls the error box into one shared helper, and tells eachgitcommand which folder to run in (cwd=) instead of changing the whole process's working directory.It also plugs a real argument-injection hole: a pull request author controls their own branch name, and we were handing that name straight to
git fetchas a bare word. A branch literally named--upload-pack=<some command>would be read by git as an option, not a branch — and git would run that command. Adding--before the branch tells git "everything after this is a name, not a flag."Security fix:
--end-of-options separator ongit fetch(667887d)pr_info.head_branchcomes straight from the GitHub API (data["head"]["ref"]) and is fully controlled by the PR author. It flowed positionally intogit fetchon both the fork and non-fork paths. Becausegit check-ref-formatacceptsrefs/heads/--upload-pack=x, neither git nor GitHub rejects such a name for us — andcomfy install --pris designed to run against untrusted forks.Verified against live git:
Both
fetchcall sites now pass["git", "fetch", <remote>, "--", head_branch]. The remaining git invocations in this function were audited and are safe by construction: the twogit checkout -Bstart-points are prefixed (origin/…,<remote>/…),local_branchis prefixedpr-, andgit remote add <name> <url>rejects a dash-leading URL outright. Regression assertions were added to bothcheckout_prtests (the suite mockssubprocess.run, so nothing would otherwise have caught a wrong refspec syntax).What else changed
_print_git_error(title, panel_title, context, details, exc)helper renders the shared rich errorPanel(bold-red title, bold-yellow context line, italic detail lines, optionalstderrblock). Bothexcept subprocess.CalledProcessErrorblocks now call it.cwd=repo_pathon everysubprocess.runcall in both functions, replacing theos.getcwd()/os.chdir(repo_path)/finally: os.chdir(original_dir)scaffolding.import osis now unused and removed.Why the
cwd=change matters (beyond dedup)os.chdirmutates process-global state. Passingcwd=per-call eliminates that mutation, so the functions are safe under any future threaded/concurrent use (no shared-cwd races) and there is no global state left to restore on the error path.To be precise about what this does not buy: a missing
repo_pathstill raises an uncaughtFileNotFoundError— the throw simply moves fromos.chdirtosubprocess.run(cwd=…). That is unchanged frommain; removing the process-global cwd mutation is the actual win.Behavior is otherwise preserved: git commands still execute against
repo_path, and each function still returnsFalseon a git failure.Behavior note (judgment call)
The
checkout_prerror panel is byte-identical to before. Thegit_checkout_tagpanel has minor cosmetic differences on the failure path, which I consider a net improvement:Text.append(f"[cyan]{tag}[/cyan]"), butText.appenddoes not parse console markup — so users literally saw[cyan]v1.2.3[/cyan](brackets and all). It now shows a cleanv1.2.3in the header line.If exact byte-for-byte preservation of the old (buggy) tag panel is preferred, I can special-case it — but the current output reads better.
Testing
tests/comfy_cli/command/github/test_pr.py— the four mockedcheckout_prtests dropped their now-dead@patch("os.chdir")/@patch("os.getcwd")decorators and now assertcwd=repo_pathis passed to everysubprocess.run, plus the two---separator regression assertions described above.main:3541 passed, 37 skipped;uvx ruff@0.15.15 format --diff .→270 files already formatted;ruff checkclean.